///////////////////////////////////////////////////////////////////////////////
// File:        RealmdPlayer.cc
// Author:      671643387@qq.com
// Description: 
///////////////////////////////////////////////////////////////////////////////

#include "Log.hpp"
#include "RealmdCode.hpp"
#include "RealmdStruct.hpp"
#include "Realmd.hpp"
#include "RealmdPlayer.hpp"

///////////////////////////////////////////////////////////////////////////////
enum eStatus
{
    STATUS_CONNECTED = 0,
    STATUS_AUTHED
};

typedef struct AuthHandler
{
    eAuthCmd    cmd;
    uint32_t    status;
    bool (RealmdPlayer::*handler)(void);
}AuthHandler;

const AuthHandler table[] =
{
    { CMD_AUTH_LOGON_CHALLENGE,         STATUS_CONNECTED,   &RealmdPlayer::_HandleLogonChallenge },
    { CMD_AUTH_LOGON_PROOF,             STATUS_CONNECTED,   &RealmdPlayer::_HandleLogonProof },
    { CMD_AUTH_RECONNECT_CHALLENGE,     STATUS_CONNECTED,   &RealmdPlayer::_HandleReconnectChallenge },
    { CMD_AUTH_RECONNECT_PROOF,         STATUS_CONNECTED,   &RealmdPlayer::_HandleReconnectProof },
    { CMD_REALM_LIST,                   STATUS_AUTHED,      &RealmdPlayer::_HandleRealmList }
};

///////////////////////////////////////////////////////////////////////////////

RealmdPlayer::RealmdPlayer(void)
: build_(0)
, authed_(false)
, accountsecLv_(SEC_PLAYER)
{

}

RealmdPlayer::~RealmdPlayer(void)
{

}

///////////////////////////////////////////////////////////////////////////////

bool RealmdPlayer::_HandleLogonChallenge(void)
{
    DEBUG_LOG("entering _HandleLogonChallenge");

    ///////////////////////////////////////////////////////////////////////////
    if (RecvLen() < sizeof(sAuthLogonChallenge_C))
        return false;

    std::vector<uint8_t> buf;
    buf.resize(4);

    Recv((char*)&buf[0], 4);

    uint16_t remaining = ((sAuthLogonChallenge_C*)&buf[0])->size;
    DEBUG_LOG("[AuthChallenge] got header, body is %#04x bytes", remaining);
    if ((remaining < sizeof(sAuthLogonChallenge_C)-buf.size() || (RecvLen() < remaining)))
        return false;

    buf.resize(remaining + buf.size() + 1);
    buf[buf.size() - 1] = 0;
    sAuthLogonChallenge_C* ch = (sAuthLogonChallenge_C*)&buf[0];

    Recv((char*)&buf[4], remaining);
    DEBUG_LOG("[AuthChallenge] got full packet, %#04x bytes", ch->size);
    DEBUG_LOG("[AuthChallenge] name(%d): '%s'", ch->I_len, ch->I);

    ///////////////////////////////////////////////////////////////////////////
    ByteBuffer pkt;

    Database* logindb = Realmd::GetDB();
    
    uname_ = (const char*)ch->I;
    build_ = ch->build;
    safeUname_ = uname_;
    logindb->EscapeString(safeUname_);

    pkt << (uint8_t)CMD_AUTH_LOGON_CHALLENGE;
    pkt << (uint8_t)0;

    std::string address = GetRemoteIP();
    logindb->EscapeString(address);
    SqlResultSetPtr result = logindb->PQuery("SELECT unbandate FROM ip_banned WHERE "
        "(unbandate = bandate OR unbandate > UNIX_TIMESTAMP()) AND ip = '%s'", address.c_str());
    if (result)
    {
        // client ip banned
        pkt << (uint8_t)WOW_FAIL_BANNED;
    }
    else
    {
        result = logindb->PQuery("SELECT sha_pass_hash, id, locked, last_ip, "
            "gmlevel, v, s FROM account WHERE username = '%s'", uname_.c_str());
        if (result)
        {
            ///////////////////////////////////////////////////////////////////
            bool locked = false;
            if ((*result)[2]->GetUInt8() == 1)
            {
                DEBUG_LOG("[AuthChallenge] Account '%s' is locked to IP - '%s'",
                    uname_.c_str(), (*result)[3]->GetCString());
                DEBUG_LOG("[AuthChallenge] Player address is '%s'", GetRemoteIP().c_str());
                if (strcmp((*result)[3]->GetCString(), GetRemoteIP().c_str()))
                {
                    DEBUG_LOG("[AuthChallenge] Account IP differs");
                    pkt << (uint8_t)WOW_FAIL_SUSPENDED;
                    locked = true;
                }
                else
                {
                    DEBUG_LOG("[AuthChallenge] Account IP matches");
                }
            }
            else
            {
                DEBUG_LOG("[AuthChallenge] Account '%s' is "
                    "not locked to ip", uname_.c_str());
            }
            ///////////////////////////////////////////////////////////////////
            if (!locked)
            {
                SqlResultSetPtr banresult = logindb->PQuery(
                    "SELECT bandate, unbandate FROM account_banned WHERE "
                    "id = %u AND active = 1 AND (unbandate > UNIX_TIMESTAMP() "
                    "OR unbandate = bandate)", (*result)[1]->GetUInt32()
                    );

                if (banresult)
                {
                    if ((*banresult)[0]->GetUInt64() == (*banresult)[1]->GetUInt64())
                    {
                        pkt << (uint8_t)WOW_FAIL_BANNED;
                        DEBUG_LOG("[AuthChallenge] Banned account %s tries to login!",
                            uname_.c_str());
                    }
                    else
                    {
                        pkt << (uint8_t)WOW_FAIL_SUSPENDED;
                        DEBUG_LOG("[AuthChallenge] Temporarily banned account %s tries to login!",
                            uname_.c_str());
                    }
                }
                else
                {
                    ///////////////////////////////////////////////////////////////////

                    ///- password sha
                    std::string rI = (*result)[0]->GetString();

                    ///- Don't calculate (v, s) if there are already some in the db
                    std::string databaseV = (*result)[5]->GetString();
                    std::string databaseS = (*result)[6]->GetString();

                    DEBUG_LOG("database authentication values: v='%s' s='%s'",
                        databaseV.c_str(), databaseS.c_str());

                    if (databaseV.size() != SRP6::s_BYTE_SIZE * 2
                        || databaseS.size() != SRP6::s_BYTE_SIZE * 2)
                    {
                        srp_.SetP(rI.c_str());
                        // update v,s in db
                        const char* v_hex, *s_hex;
                        v_hex = srp_.Get_v().AsHexStr();
                        s_hex = srp_.Get_s().AsHexStr();
                        Realmd::GetDB()->PExecute(
                            "UPDATE account SET v = '%s', s = '%s' WHERE username = '%s';",
                            v_hex, s_hex, safeUname_.c_str());
                        BigNumber::Free((void*)v_hex);
                        BigNumber::Free((void*)s_hex);
                    }
                    else
                        srp_.SetVS(databaseV.c_str(), databaseS.c_str());

                    srp_.DoCalculateStepOne();

                    BigNumber unk3;
                    unk3.SetRand(16 * 8);

                    // fill packet
                    ///////////////////////////////////////////////////////////
                    pkt << (uint8_t)WOW_SUCCESS;

                    pkt.append(srp_.Get_B().AsByteArray(32), 32);
                    pkt << (uint8_t)1;
                    pkt.append(srp_.Get_g().AsByteArray(), 1);
                    pkt << (uint8_t)32;
                    pkt.append(srp_.Get_N().AsByteArray(32), 32);
                    pkt.append(srp_.Get_s().AsByteArray(), srp_.Get_s().GetNumBytes());
                    pkt.append(unk3.AsByteArray(16), 16);

                    uint8_t securityFlags = 0;
                    pkt << (uint8_t)securityFlags;

                    if (securityFlags & 0x01)
                    {
                        pkt << (uint32_t)0;
                        pkt << uint64_t(0) << (uint64_t)0;
                    }

                    if (securityFlags & 0x02)
                    {
                        pkt << uint8_t(0);
                        pkt << uint8_t(0);
                        pkt << uint8_t(0);
                        pkt << uint8_t(0);
                        pkt << uint64_t(0);
                    }

                    if (securityFlags & 0x04)
                        pkt << uint8_t(1);

                    ///////////////////////////////////////////////////////////

                    uint8_t seclv = (*result)[4]->GetUInt8();
                    accountsecLv_ = seclv <= SEC_ADMINISTRATOR ? eAccountTypes(seclv) : SEC_ADMINISTRATOR;

                    localizationName_.resize(4);
                    for (int x = 0; x < 4; ++x)
                        localizationName_[x] = ch->country[4 - x - 1];

                    INFO_LOG("[AuthChallenge] account %s is using '%c%c%c%c' locale (%u)",
                        uname_.c_str(), ch->country[3], ch->country[2], ch->country[1],
                        ch->country[0], GetLocaleByName(localizationName_));
                    ///////////////////////////////////////////////////////////////////
                }
            }
            ///////////////////////////////////////////////////////////////////
        }
        else
        {
            // unknown account
            pkt << (uint8_t)WOW_FAIL_UNKNOWN_ACCOUNT;
        }
    }

    Send((const char*)pkt.contents(), pkt.size());
    return true;
}

bool RealmdPlayer::_HandleLogonProof(void)
{
    DEBUG_LOG("Entering _HandleLogonProof");

    sAuthLogonProof_C lp;
    if (RecvLen() < sizeof(sAuthLogonProof_C))
        return false;

    Recv((char*)&lp, sizeof(sAuthLogonProof_C));

    // TODO
    bool valid_version = (build_ == 6141);

    if (!valid_version)
    {
        // TODO:
        ByteBuffer pkt;
        pkt << (uint8_t)CMD_AUTH_LOGON_CHALLENGE;
        pkt << (uint8_t)0x00;
        pkt << (uint8_t)WOW_FAIL_VERSION_INVALID;
        DEBUG_LOG("[AuthChallenge] %u is not a valid client version!", build_);
        Send((char const*)pkt.contents(), pkt.size());
        return true;
    }

    ///- Continue the SRP6 calculation
    BigNumber A;

    A.SetBinary(lp.A, 32);
    if (A.IsZero())
        return false;

    BigNumber M;
    srp_.DoCalculateStepTwo(A, M, uname_);

    if (!memcmp(M.AsByteArray(), lp.M1, 20))
    {
        INFO_LOG("User '%s' successfully authenticated", uname_.c_str());

        const char* K_hex = srp_.Get_K().AsHexStr();
        assert(Realmd::GetDB()->PExecute(
            "UPDATE account SET sessionkey = '%s', last_ip = '%s', last_login = NOW(), locale = '%u', "
            "failed_logins = 0 WHERE username = '%s';",
            K_hex, GetRemoteIP().c_str(), GetLocaleByName(localizationName_),
            safeUname_.c_str()));
        BigNumber::Free((void*)K_hex);

        Sha1Hash sha;
        sha.UpdateBigNumbers(&A, &M, &srp_.Get_K(), NULL);
        sha.Finalize();

        _SendProof(sha);
        authed_ = true;
    }
    else
    {
        if (build_ > 6005)
        {
            char data[4] = { CMD_AUTH_LOGON_PROOF, WOW_FAIL_UNKNOWN_ACCOUNT, 3, 0 };
            Send(data, sizeof(data));
        }
        else
        {
            char data[2] = { CMD_AUTH_LOGON_PROOF, WOW_FAIL_UNKNOWN_ACCOUNT };
            Send(data, sizeof(data));
        }

        INFO_LOG("[AuthChallenge] account %s tried to login with wrong password!", uname_.c_str());
    }

    return true;
}

bool RealmdPlayer::_HandleReconnectChallenge(void)
{
    DEBUG_LOG("Entering _HandleReconnectChallenge");
    if (RecvLen() < sizeof(sAuthLogonChallenge_C))
        return false;

    ///- Read the first 4 bytes (header) to get the length of the remaining of the packet
    std::vector<uint8_t> buf;
    buf.resize(4);

    Recv((char*)&buf[0], 4);

    uint16_t remaining = ((sAuthLogonChallenge_C*)&buf[0])->size;
    DEBUG_LOG("[ReconnectChallenge] got header, body is %#04x bytes", remaining);

    if ((remaining < sizeof(sAuthLogonChallenge_C) - buf.size()) || (RecvLen() < remaining))
    { return false; }

    // No big fear of memory outage (size is int16, i.e. < 65536)
    buf.resize(remaining + buf.size() + 1);
    buf[buf.size() - 1] = 0;
    sAuthLogonChallenge_C* ch = (sAuthLogonChallenge_C*)&buf[0];

    ///- Read the remaining of the packet
    Recv((char*)&buf[4], remaining);
    DEBUG_LOG("[ReconnectChallenge] got full packet, %#04x bytes", ch->size);
    DEBUG_LOG("[ReconnectChallenge] name(%d): '%s'", ch->I_len, ch->I);

    Database* logindb = Realmd::GetDB();

    uname_ = (const char*)ch->I;
    safeUname_ = uname_;
    logindb->EscapeString(safeUname_);
    build_ = ch->build;

    SqlResultSetPtr result = logindb->PQuery("SELECT sessionkey FROM account WHERE username = '%s'", safeUname_.c_str());

    // Stop if the account is not found
    if (!result)
    {
        ERROR_LOG("[ERROR] user %s tried to login and we can not find his session key in the database.", uname_.c_str());
        Close();
        return false;
    }
    srp_.Get_K().SetHexStr((*result)[0]->GetCString());

    ///- Sending response
    ByteBuffer pkt;
    pkt << (uint8_t)  CMD_AUTH_RECONNECT_CHALLENGE;
    pkt << (uint8_t)  0x00;
    srp_.Get_RP().SetRand(16 * 8);
    pkt.append(srp_.Get_RP().AsByteArray(16), 16);              // 16 bytes random
    pkt << (uint64_t) 0x00 << (uint64_t) 0x00;                  // 16 bytes zeros
    Send((char const*)pkt.contents(), pkt.size());
    return true;
}

bool RealmdPlayer::_HandleReconnectProof(void)
{
    DEBUG_LOG("Entering _HandleReconnectProof");
    ///- Read the packet
    sAuthReconnectProof_C lp;
    if (!Recv((char*)&lp, sizeof(sAuthReconnectProof_C)))
        return false;

    if (uname_.empty() || !srp_.Get_RP().GetNumBytes() || !srp_.Get_K().GetNumBytes())
        return false;

    BigNumber t1;
    t1.SetBinary(lp.R1, 16);

    Sha1Hash sha;
    sha.Initialize();
    sha.UpdateData(uname_);
    sha.UpdateBigNumbers(&t1, &(srp_.Get_RP()), &(srp_.Get_K()), NULL);
    sha.Finalize();

    if (!memcmp(sha.GetDigest(), lp.R2, SHA_DIGEST_LENGTH))
    {
        ///- Sending response
        ByteBuffer pkt;
        pkt << (uint8_t)  CMD_AUTH_RECONNECT_PROOF;
        pkt << (uint8_t)  0x00;
        //If we keep from sending this we don't receive Session Expired on the client when
        //changing realms after being logged on to the world
        if (build_ > 6141) // Last vanilla, 1.12.3
            pkt << (uint16_t) 0x00;                               // 2 bytes zeros
        Send((char const*)pkt.contents(), pkt.size());

        ///- Set _authed to true!
        authed_ = true;
        return true;
    }
    else
    {
        ERROR_LOG("[ERROR] user %s tried to login, but session invalid.", uname_.c_str());
        Close();
        return false;
    }
}

bool RealmdPlayer::_HandleRealmList(void)
{
    DEBUG_LOG("Entering _HandleRealmList");
    if (RecvLen() < 5)
        return false;
    Skip(5);

    SqlResultSetPtr result = Realmd::GetDB()->PQuery(
        "SELECT id, sha_pass_hash FROM account WHERE username='%s'", safeUname_.c_str());
    if (!result)
    {
        ERROR_LOG("[ERROR] user %s tried to login and we can not find him in the database.", uname_.c_str());
        Close();
        return false;
    }

    uint32_t id = (*result)[0]->GetUInt32();
    std::string rI = (*result)[1]->GetString();

    ByteBuffer rmpkt;
    _LoadRealmlist(rmpkt, id);

    ByteBuffer pkt;
    pkt << (uint8_t) CMD_REALM_LIST;
    pkt << (uint16_t) rmpkt.size();
    pkt.append(rmpkt);

    Send((const char*)pkt.contents(), pkt.size());
    return true;
}

///////////////////////////////////////////////////////////////////////////////
void RealmdPlayer::OnRead(void)
{
    uint8_t cmd;
    while (true)
    {
        if (!Peek((char*)&cmd, sizeof(cmd)))
            return;

        std::size_t x;
        for (x = 0; x < AUTH_TOTAL_COMMANDS; ++x)
        {
            if (((uint8_t)table[x].cmd == cmd) && (
                (table[x].status == STATUS_CONNECTED || (
                (authed_ && table[x].status == STATUS_AUTHED)))
                ))
            {
                DEBUG_LOG("[realmd] got data for cmd %u recv length %u",
                    (uint32_t)cmd, (uint32_t)RecvLen());

                if (!(*this.*table[x].handler)())
                {
                    DEBUG_LOG("command handler failed for cmd %u recv length %u",
                        (uint32_t)cmd, (uint32_t)RecvLen());
                    return;
                }

                break;
            }
        }

        if (x == AUTH_TOTAL_COMMANDS)
        {
            DEBUG_LOG("[realmd] got unknown packet %u", (uint32_t)cmd);
            Close();
        }
        return;
    }
}

void RealmdPlayer::OnClose(void)
{

}

///////////////////////////////////////////////////////////////////////////////
void RealmdPlayer::_SendProof(Sha1Hash& sha)
{
    switch (build_)
    {
        case 5875:                                          // 1.12.1
        case 6005:                                          // 1.12.2
        case 6141:                                          // 1.12.3
        {
            sAuthLogonProof_S_BUILD_6005 proof;
            memcpy(proof.M2, sha.GetDigest(), 20);
            proof.cmd = CMD_AUTH_LOGON_PROOF;
            proof.error = 0;
            proof.unk2 = 0x00;

            Send((char*)&proof, sizeof(proof));
            break;
        }
    }
}

void RealmdPlayer::_LoadRealmlist(ByteBuffer& pkt, uint32_t id)
{
    Realmd::GetRealmList()->Update();

    pkt << uint32_t(0);
    pkt << uint8_t(Realmd::GetRealmList()->GetRealmCount());

    RealmList::iterator iter = Realmd::GetRealmList()->Begin();
    for (; iter != Realmd::GetRealmList()->End(); ++ iter)
    {
        SqlResultSetPtr result = Realmd::GetDB()->PQuery(
            "SELECT numchars FROM realmcharacters WHERE realmid = %d AND acctid = %d;",
            iter->id, id);

        uint8_t chars = 0;
        if (result)
            chars = (*result)[0]->GetUInt8();

        pkt << uint32_t(iter->icon);            // icon
        pkt << uint8_t(iter->realmflags);       // realm flag
        pkt << iter->name;                      // realm name
        pkt << iter->address;                   // realm address
        pkt << iter->populationLevel;           // population
        pkt << chars;                           // amount of characters
        pkt << iter->timezone;                  // timezone
        pkt << uint8_t(0x00);                   // unk
    }

    pkt << uint16_t(0x0002);
}